package com.robusta.app.auth.service;

import com.robusta.app.auth.api.AuthenticationFailedException;
import com.robusta.app.auth.api.PasswordEncoder;
import com.robusta.app.auth.api.SessionTokenGenerator;
import com.robusta.app.auth.api.UserAuthentication;
import com.robusta.app.auth.model.UserModel;
import com.robusta.app.auth.model.UserSessionModel;
import com.robusta.app.auth.persistence.UserRepository;
import com.robusta.app.auth.persistence.UserSessionRepository;
import com.robusta.app.domain.user.User;
import com.robusta.app.domain.user.UserContextHolder;
import com.robusta.app.domain.user.UserSession;
import com.robusta.app.domain.user.UserType;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.persistence.NoResultException;

import static com.google.common.base.Preconditions.checkState;

@Service
public class DBLookupUserAuthentication implements UserAuthentication {
    private PasswordEncoder encoder;
    private UserRepository userRepository;
    private UserSessionRepository sessionRepository;
    private SessionTokenGenerator tokenGenerator;

    @Autowired
    public DBLookupUserAuthentication(PasswordEncoder encoder, UserRepository userRepository, UserSessionRepository sessionRepository, SessionTokenGenerator tokenGenerator) {
        this.encoder = encoder;
        this.userRepository = userRepository;
        this.sessionRepository = sessionRepository;
        this.tokenGenerator = tokenGenerator;
    }

    @Override
    @Transactional
    public UserSession authenticateAndCreateUserSession(String loginName, String plainTextPassword) throws AuthenticationFailedException {
        String encodedPassword = encoder.encodePlainTextPassword(plainTextPassword);
        UserModel model;
        try {
            model = userRepository.lookupUserByCredentials(loginName, encodedPassword);
        } catch (NoResultException e) {
            throw new AuthenticationFailedException(String.format("Could not authenticate user: %s", loginName), e);
        }
        User user = new FromUserModel(model).toUser();
        UserContextHolder.setCurrentUser(user); // User session save will require this.
        sessionRepository.inactivateCurrentSessionsForUser(model.getId());
        UserSessionModel userSession = sessionRepository.createSessionForUserId(model.getId(), tokenGenerator.generate());

        return new FromUserSessionModel(userSession).withUser(user).toUserSession();
    }

    @Override
    @Cacheable("userSessions")
    @Transactional(readOnly = true)
    public UserSession lookupUserSessionByToken(String authToken) {
        UserSessionModel userSessionModel = sessionRepository.lookupUserSessionByToken(authToken);
        checkState(userSessionModel != null, "Lookup of active session by authentication token: %s failed. Service returned null.", authToken);
        return new FromUserSessionModel(userSessionModel).withUser(new FromUserModel(userSessionModel.getLoggedInUser()).toUser()).toUserSession();
    }

    private class FromUserSessionModel {
        private UserSessionModel model;
        private User user;

        private FromUserSessionModel(UserSessionModel model) {
            this.model = model;
        }

        private UserSession toUserSession() {
            return new AuthUserSessionImpl(user, model.getToken());
        }

        public FromUserSessionModel withUser(User user) {
            this.user = user; return this;
        }

        private class AuthUserSessionImpl implements UserSession {
            private User user;
            private String token;

            private AuthUserSessionImpl(User user, String token) {
                this.user = user;
                this.token = token;
            }

            @Override
            public User loggedUser() {
                return user;
            }

            @Override
            public Authentication authentication() {
                return new Authentication() {
                    @Override
                    public String authToken() {
                        return token;
                    }
                };
            }
        }
    }

    private class FromUserModel {
        private UserModel model;

        private FromUserModel(UserModel model) {
            this.model = model;
        }

        private User toUser() {
            return new AuthUserImpl(model.getName(), model.getUserType(), model.getLoginName(), model.getId());
        }

        private class AuthUserImpl implements User {
            private String name;
            private UserType userType;
            private String loginName;
            private Long id;

            private AuthUserImpl(String name, UserType userType, String loginName, Long id) {
                this.name = name;
                this.userType = userType;
                this.loginName = loginName;
                this.id = id;
            }

            @Override
            public String name() {
                return name;
            }

            @Override
            public UserType type() {
                return userType;
            }

            @Override
            public String loginName() {
                return loginName;
            }

            @Override
            public String identifier() {
                return String.valueOf(id);
            }
        }
    }
}
